# 资源压缩 zlib
30 秒速记
zlib是Node.js内置压缩模块:gzip/deflate/brotli都有,流式和一次性两套API- 流式用
pipeline串文件流或HTTP流,自动处理背压和错误传播 - 压不压看四件事:客户端
Accept-Encoding、内容类型、体积、CPU→ 图片视频已经压过,别再压 - 动态压缩设最小体积阈值,响应加
Vary: Accept-Encoding - 解压不可信数据要限制输出体积,防压缩炸弹
zlib 是 Node.js 自带的压缩模块,能做 gzip、deflate、brotli,平时用得最多的是把它当成一个「转换流」接在读写流中间。 比如读文件 → 压缩 → 写文件,或者读文件 → 压缩 → 写进 HTTP 响应。我一般用 stream.pipeline 来串,它会处理背压,也能在任何一段出错时把整条链路关掉。是否压缩要看收益:文本类资源压完通常能小 70% 以上,图片视频本身已经压过,再压只是白费 CPU。解压外部数据时还得限制输出大小,不然一个几 KB 的包可能解出几个 GB。
理解「资源压缩 zlib」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
zlib 有两套 API,选错了在线上会很明显。
一套是一次性的:zlib.gzipSync(buf)、zlib.gzip(buf, cb),输入输出都是完整的 Buffer,适合几 KB 的字符串。Sync 版会阻塞事件循环,在请求处理里压大内容,整个服务都会卡住。
另一套是流式的:zlib.createGzip()、createGunzip()、createBrotliCompress() 等,返回一个 Transform 流,数据分块进、分块出,内存占用和文件大小无关。
const fs = require('fs');
const zlib = require('zlib');
const { pipeline } = require('stream/promises');
async function gzipFile(src) {
await pipeline(
fs.createReadStream(src),
zlib.createGzip({ level: 6 }), // 1 最快,9 压得最小
fs.createWriteStream(src + '.gz'),
);
console.log('done'); // 任一环节出错都会 reject,流也会被自动销毁
}
gzipFile('./app.js').catch((e) => { console.error(e); process.exitCode = 1; });
几个容易踩的坑:压缩本身有固定开销,'hello world' 压完是 31 字节,比原文还大,所以动态压缩要设阈值(常见是 1KB);brotli 压缩率比 gzip 高,但高等级压缩很慢,适合构建时预压缩,不适合每个请求实时压;解压不可信输入时,一次性 API 可以传 maxOutputLength,超出会抛 ERR_BUFFER_TOO_LARGE,流式解压则要自己数输出字节,超了就 destroy。
面试官追问
追问 1压缩脚本跑完只检查目标文件存在就算成功,哪里不够?
输入损坏或中途出错时,目标文件可能已经建出来了,只是半截。要以 pipeline 成功结束为准,失败时删掉半成品、设置非零退出码,最好再解压一遍校验内容。
追问 2静态资源要压上千个文件,一次性全开流会怎样?
文件句柄和内存会一起涨,可能直接 EMFILE。用一个并发上限,比如同时处理 8 个,逐个记录失败,最后汇总退出码。
追问 3构建时压缩失败,是继续发布原文件还是阻断 CI?
看产物是否关键。允许降级的话,也要把失败打出来、单独告警,不能吞掉;关键产物直接阻断。别让「为了可用性」变成静默忽略错误。
追问 4压缩任务结束后进程迟迟不退出,查什么?
查是不是有流没关、句柄没释放,比如出错分支里只关了读流没关写流。用 pipeline 能省掉大部分这类问题;最后靠 process.exit() 硬退只是掩盖。
# 概览
30 秒速记
- 流程:浏览器在
Accept-Encoding里报能力 → 服务端挑一种压缩 → 用Content-Encoding告诉浏览器 - 常见编码:
gzip(兼容最好)、br(压得更小,HTTPS下普遍支持)、deflate(基本没人用) - 文本类资源(
JS/CSS/HTML/JSON)收益大;图片、视频、压缩包别再压 - 缓存层要加
Vary: Accept-Encoding,否则会把压缩版发给不支持的客户端 - 压缩只在一层做:网关做了,应用就别再做
gzip 压缩是一次协商:浏览器在请求头 Accept-Encoding 里说自己支持什么,服务端挑一种压好,在响应头 Content-Encoding 里写明用了哪种。 浏览器拿到后自动解压,业务代码完全无感。Node.js 里干这活的就是 zlib 模块。实际项目里我更关心的是边界:图片视频本来就压过,别再压;有 CDN 缓存时要带上 Vary: Accept-Encoding;还有压缩到底放在 Nginx 还是 Node.js 里,选一层就好,两层都开容易出乱子。
做过web性能优化的同学,对性能优化大杀器gzip应该不陌生。浏览器向服务器发起资源请求,比如下载一个js文件,服务器先对资源进行压缩,再返回给浏览器,以此节省流量,加快访问速度。
浏览器通过HTTP请求头部里加上Accept-Encoding,告诉服务器,“你可以用gzip,或者defalte算法压缩资源”。
Accept-Encoding:gzip, deflate
那么,在nodejs里,是如何对资源进行压缩的呢?答案就是Zlib模块。
面试官追问
追问 1浏览器带了 Accept-Encoding: gzip,请求的是 JPG,要不要压?
不要。Accept-Encoding 只说明它能解,不代表每个资源都值得压。JPG 本身就是压缩格式,再 gzip 体积几乎不变,CPU 白花。
追问 2抓包看到响应体是压缩数据,但没有 Content-Encoding 头,会怎样?
浏览器会把压缩后的字节当成原始内容,JS 直接解析失败,页面一堆乱码报错。先查服务端是否漏写了头,再看代理有没有把这个头删了。
追问 3CDN 只按 URL 缓存,旧客户端收到了 gzip 内容解不开,怎么修?
源站响应加 Vary: Accept-Encoding,让缓存按客户端的编码能力分开存。再确认 CDN 是否认这个头,有的 CDN 要在控制台单独开。
追问 4网关已经开了 gzip,Node.js 里又压一次,会出什么事?
轻则白费 CPU,重则内容被编码两次、头和内容对不上,客户端解压失败。只让一层负责,一般交给 Nginx 或 CDN,应用返回原始内容。
# 入门实例:简单的压缩/解压缩
30 秒速记
- 压缩:
createReadStream→createGzip()→createWriteStream('x.gz') - 解压:
createReadStream('x.gz')→createGunzip()→createWriteStream - 生产里用
pipeline代替一串pipe,出错能统一捕获、自动关流 - 成功的判据:
pipeline正常结束 + 解压回来和原文一致,而不是「文件存在」 - 解压不可信数据要限制输出大小
压缩和解压在 zlib 里是对称的:读流 → 压缩或解压转换流 → 写流,中间换一个 createGzip 或 createGunzip 就行。 数据是一块一块流过去的,文件再大内存也稳定。入门例子常用 a.pipe(b).pipe(c),但 pipe 不会把错误往下传,读文件失败时写流可能一直挂着,所以正式代码我都换成 pipeline。验证也别只看有没有生成 .gz,最好压完再解回来和原文比一下,再拿一个损坏的文件跑一遍,确认失败路径能报错、能退出。
理解「入门实例:简单的压缩/解压缩」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
完整的压缩 + 解压往返例子,顺便把失败路径也覆盖上:
const fs = require('fs');
const zlib = require('zlib');
const { pipeline } = require('stream/promises');
async function roundTrip() {
fs.writeFileSync('a.txt', 'hello zlib\n'.repeat(1000));
await pipeline(fs.createReadStream('a.txt'), zlib.